package cn.hutool.json.serialize;

import cn.hutool.core.convert.Convert;
import cn.hutool.core.convert.NumberWithFormat;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.TemporalAccessorUtil;
import cn.hutool.core.date.format.GlobalCustomFormat;
import cn.hutool.core.io.IORuntimeException;
import cn.hutool.core.lang.Filter;
import cn.hutool.core.lang.mutable.MutablePair;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.CharUtil;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.*;
import com.particle.global.tool.json.CustomJSONConfig;

import java.io.IOException;
import java.io.Writer;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.MonthDay;
import java.time.temporal.TemporalAccessor;
import java.util.Calendar;
import java.util.Date;
import java.util.Iterator;
import java.util.Map;

/**
 * JSON数据写出器<br>
 * 通过简单的append方式将JSON的键值对等信息写出到{@link Writer}中。
 *
 * @author looly
 * @since 5.7.3
 */
public class JSONWriter extends Writer {

    /**
     * 缩进因子，定义每一级别增加的缩进量
     */
    private final int indentFactor;
    /**
     * 本级别缩进量
     */
    private final int indent;
    /**
     * Writer
     */
    private final Writer writer;
    /**
     * JSON选项
     */
    private final JSONConfig config;

    /**
     * 写出当前值是否需要分隔符
     */
    private boolean needSeparator;
    /**
     * 是否为JSONArray模式
     */
    private boolean arrayMode;

    /**
     * 创建JSONWriter
     *
     * @param writer       {@link Writer}
     * @param indentFactor 缩进因子，定义每一级别增加的缩进量
     * @param indent       本级别缩进量
     * @param config       JSON选项
     * @return JSONWriter
     */
    public static JSONWriter of(Writer writer, int indentFactor, int indent, JSONConfig config) {
        return new JSONWriter(writer, indentFactor, indent, config);
    }

    /**
     * 构造
     *
     * @param writer       {@link Writer}
     * @param indentFactor 缩进因子，定义每一级别增加的缩进量
     * @param indent       本级别缩进量
     * @param config       JSON选项
     */
    public JSONWriter(Writer writer, int indentFactor, int indent, JSONConfig config) {
        this.writer = writer;
        this.indentFactor = indentFactor;
        this.indent = indent;
        this.config = config;
    }

    /**
     * JSONObject写出开始，默认写出"{"
     *
     * @return this
     */
    public JSONWriter beginObj() {
        writeRaw(CharUtil.DELIM_START);
        return this;
    }

    /**
     * JSONArray写出开始，默认写出"["
     *
     * @return this
     */
    public JSONWriter beginArray() {
        writeRaw(CharUtil.BRACKET_START);
        arrayMode = true;
        return this;
    }

    /**
     * 结束，默认根据开始的类型，补充"}"或"]"
     *
     * @return this
     */
    public JSONWriter end() {
        // 换行缩进
        writeLF().writeSpace(indent);
        writeRaw(arrayMode ? CharUtil.BRACKET_END : CharUtil.DELIM_END);
        flush();
        arrayMode = false;
        // 当前对象或数组结束，当新的
        needSeparator = true;
        return this;
    }

    /**
     * 写出键，自动处理分隔符和缩进，并包装键名
     *
     * @param key 键名
     * @return this
     */
    public JSONWriter writeKey(String key) {
        if (needSeparator) {
            writeRaw(CharUtil.COMMA);
        }
        // 换行缩进
        writeLF().writeSpace(indentFactor + indent);
        return writeRaw(JSONUtil.quote(key));
    }

    /**
     * 写出值，自动处理分隔符和缩进，自动判断类型，并根据不同类型写出特定格式的值<br>
     * 如果写出的值为{@code null}或者{@link JSONNull}，且配置忽略null，则跳过。
     *
     * @param value 值
     * @return this
     */
    public JSONWriter writeValue(Object value) {
        if(JSONUtil.isNull(value) && config.isIgnoreNullValue()){
            return this;
        }
        return writeValueDirect(value, null);
    }

    /**
     * 写出字段名及字段值，如果字段值是{@code null}且忽略null值，则不写出任何内容
     *
     * @param key 字段名
     * @param value 字段值
     * @return this
     * @since 5.7.6
     * @deprecated 请使用 {@link #writeField(MutablePair, Filter)}
     */
    @Deprecated
    public JSONWriter writeField(String key, Object value){
        if(JSONUtil.isNull(value) && config.isIgnoreNullValue()){
            return this;
        }
        return writeKey(key).writeValueDirect(value, null);
    }

    /**
     * 写出字段名及字段值，如果字段值是{@code null}且忽略null值，则不写出任何内容
     *
     * @param pair 键值对
     * @param filter 键值对的过滤器，可以编辑键值对
     * @return this
     * @since 5.8.6
     */
    public JSONWriter writeField(MutablePair<Object, Object> pair, Filter<MutablePair<Object, Object>> filter){
        if(JSONUtil.isNull(pair.getValue()) && config.isIgnoreNullValue()){
            return this;
        }

        if (null == filter || filter.accept(pair)) {
            if(false == arrayMode){
                // JSONArray只写值，JSONObject写键值对
                writeKey(StrUtil.toString(pair.getKey()));
            }
            return writeValueDirect(pair.getValue(), filter);
        }
        return this;
    }

    @Override
    public void write(char[] cbuf, int off, int len) throws IOException {
        this.writer.write(cbuf, off, len);
    }

    @Override
    public void flush() {
        try {
            this.writer.flush();
        } catch (IOException e) {
            throw new IORuntimeException(e);
        }
    }

    @Override
    public void close() throws IOException {
        this.writer.close();
    }

    // ------------------------------------------------------------------------------ Private methods
    /**
     * 写出值，自动处理分隔符和缩进，自动判断类型，并根据不同类型写出特定格式的值
     *
     * @param value 值
     * @param filter 键值对过滤器
     * @return this
     */
    private JSONWriter writeValueDirect(Object value, Filter<MutablePair<Object, Object>> filter) {
        if (arrayMode) {
            if (needSeparator) {
                writeRaw(CharUtil.COMMA);
            }
            // 换行缩进
            writeLF().writeSpace(indentFactor + indent);
        } else {
            writeRaw(CharUtil.COLON).writeSpace(1);
        }
        needSeparator = true;
        return writeObjValue(value, filter);
    }

    /**
     * 写出JSON的值，根据值类型不同，输出不同内容
     *
     * @param value 值
     * @param filter 过滤器
     * @return this
     */
    private JSONWriter writeObjValue(Object value, Filter<MutablePair<Object, Object>> filter) {
        final int indent = indentFactor + this.indent;
        if (value == null || value instanceof JSONNull) {
            writeRaw(JSONNull.NULL.toString());
        } else if (value instanceof JSON) {
            if(value instanceof JSONObject){
                ((JSONObject) value).write(writer, indentFactor, indent, filter);
            }else if(value instanceof JSONArray){
                ((JSONArray) value).write(writer, indentFactor, indent, filter);
            }
        } else if (value instanceof Map || value instanceof Map.Entry) {
            new JSONObject(value).write(writer, indentFactor, indent);
        } else if (value instanceof Iterable || value instanceof Iterator || ArrayUtil.isArray(value)) {
            new JSONArray(value).write(writer, indentFactor, indent);
        } else if (value instanceof Number) {
            // issue#IALQ0N，避免设置日期格式后writeLongAsString失效
            if(value instanceof NumberWithFormat){
                value = ((NumberWithFormat) value).getNumber();
            }

            if(value instanceof Long && config.isWriteLongAsString()){
                // issue#3541
                // long可能溢出，此时可选是否将long写出为字符串类型
                writeStrValue(value.toString());
            } else {
                writeNumberValue((Number) value);
            }
        } else if (value instanceof Date || value instanceof Calendar || value instanceof TemporalAccessor) {
            // issue#2572@Github
            if(value instanceof MonthDay){
                writeStrValue(value.toString());
                return this;
            }


            String format = (null == config) ? null : config.getDateFormat();
            // 修改开始
            if (config instanceof CustomJSONConfig) {
                if (value instanceof LocalDate) {
                    format = (null == config) ? null : ((CustomJSONConfig) config).getLocalDateFormat();
                }else if (value instanceof LocalDateTime) {
                    format = (null == config) ? null : ((CustomJSONConfig) config).getLocalDateTimeFormat();
                }
            }
            // 修改结束

            writeRaw(formatDate(value, format));
        } else if (value instanceof Boolean) {
            writeBooleanValue((Boolean) value);
        } else if (value instanceof JSONString) {
            writeJSONStringValue((JSONString) value);
        } else {
            writeStrValue(value.toString());
        }

        return this;
    }

    /**
     * 写出数字，根据{@link JSONConfig#isStripTrailingZeros()} 配置不同，写出不同数字<br>
     * 主要针对Double型是否去掉小数点后多余的0<br>
     * 此方法输出的值不包装引号。
     *
     * @param number 数字
     */
    private void writeNumberValue(Number number) {
        // since 5.6.2可配置是否去除末尾多余0，例如如果为true,5.0返回5
        final boolean isStripTrailingZeros = null == config || config.isStripTrailingZeros();
        writeRaw(NumberUtil.toStr(number, isStripTrailingZeros));
    }

    /**
     * 写出Boolean值，直接写出true或false,不适用引号包装
     *
     * @param value Boolean值
     */
    private void writeBooleanValue(Boolean value) {
        writeRaw(value.toString());
    }

    /**
     * 输出实现了{@link JSONString}接口的对象，通过调用{@link JSONString#toJSONString()}获取JSON字符串<br>
     * {@link JSONString}按照JSON对象对待，此方法输出的JSON字符串不包装引号。<br>
     * 如果toJSONString()返回null，调用toString()方法并使用双引号包装。
     *
     * @param jsonString {@link JSONString}
     */
    private void writeJSONStringValue(JSONString jsonString) {
        String valueStr;
        try {
            valueStr = jsonString.toJSONString();
        } catch (Exception e) {
            throw new JSONException(e);
        }
        if (null != valueStr) {
            writeRaw(valueStr);
        } else {
            writeStrValue(jsonString.toString());
        }
    }

    /**
     * 写出字符串值，并包装引号并转义字符<br>
     * 对所有双引号做转义处理（使用双反斜杠做转义）<br>
     * 为了能在HTML中较好的显示，会将&lt;/转义为&lt;\/<br>
     * JSON字符串中不能包含控制字符和未经转义的引号和反斜杠
     *
     * @param csq 字符串
     */
    private void writeStrValue(String csq) {
        try {
            JSONUtil.quote(csq, writer);
        } catch (IOException e) {
            throw new IORuntimeException(e);
        }
    }

    /**
     * 写出空格
     *
     * @param count 空格数
     */
    private void writeSpace(int count) {
        if (indentFactor > 0) {
            for (int i = 0; i < count; i++) {
                writeRaw(CharUtil.SPACE);
            }
        }
    }

    /**
     * 写出换换行符
     *
     * @return this
     */
    private JSONWriter writeLF() {
        if (indentFactor > 0) {
            writeRaw(CharUtil.LF);
        }
        return this;
    }

    /**
     * 写入原始字符串值，不做任何处理
     *
     * @param csq 字符串
     * @return this
     */
    private JSONWriter writeRaw(String csq) {
        try {
            writer.append(csq);
        } catch (IOException e) {
            throw new IORuntimeException(e);
        }
        return this;
    }

    /**
     * 写入原始字符值，不做任何处理
     *
     * @param c 字符串
     * @return this
     */
    private JSONWriter writeRaw(char c) {
        try {
            writer.write(c);
        } catch (IOException e) {
            throw new IORuntimeException(e);
        }
        return this;
    }

    /**
     * 按照给定格式格式化日期，格式为空时返回时间戳字符串
     *
     * @param dateObj Date或者Calendar对象
     * @param format  格式
     * @return 日期字符串
     */
    private static String formatDate(Object dateObj, String format) {
        if (StrUtil.isNotBlank(format)) {
            final String dateStr;
            if (dateObj instanceof TemporalAccessor) {
                dateStr = TemporalAccessorUtil.format((TemporalAccessor) dateObj, format);
            } else {
                dateStr = DateUtil.format(Convert.toDate(dateObj), format);
            }

            if (GlobalCustomFormat.FORMAT_SECONDS.equals(format)
                    || GlobalCustomFormat.FORMAT_MILLISECONDS.equals(format)) {
                // Hutool自定义的秒和毫秒表示，默认不包装双引号
                return dateStr;
            }
            //用户定义了日期格式
            return JSONUtil.quote(dateStr);
        }

        //默认使用时间戳
        long timeMillis;
        if (dateObj instanceof TemporalAccessor) {
            timeMillis = TemporalAccessorUtil.toEpochMilli((TemporalAccessor) dateObj);
        } else if (dateObj instanceof Date) {
            timeMillis = ((Date) dateObj).getTime();
        } else if (dateObj instanceof Calendar) {
            timeMillis = ((Calendar) dateObj).getTimeInMillis();
        } else {
            throw new UnsupportedOperationException("Unsupported Date type: " + dateObj.getClass());
        }
        return String.valueOf(timeMillis);
    }
}
